import { defineComponent, onUnmounted, ref, watchEffect } from "vue";

export interface VirtualListCoreProps {
    height?: number,
    maxHeight?: number,
    itemEstimatedSize: number
    overscan?: number,
    items: any[],
    scrollElement: any,
    contentElement: any,
    bodyElement: any,
}

export interface MeasuredData {
    size: number,
    offset: number,
}
export interface IMeasuredDataMap {
    [key: number]: MeasuredData
}

export default defineComponent({
    name: 'VirtualListCore',
    props: {
        height: {type: Number, default: undefined},
        maxHeight: {type: Number, default: undefined},
        itemEstimatedSize: {type: Number, default: 20, required: true},
        overscan: {type: Number, default: undefined},
        items: {type: Array, default: [], required: true},
        scrollElement: {type: Function, required: true},
        contentElement: {type: Function, required: true},
        bodyElement: {type: Function, required: true},
    },
    setup (props: VirtualListCoreProps, {slots, expose}) {
        const containerHeight = props.height ?? props.maxHeight;
        if (containerHeight === undefined) {
            console.error('Virtual List need height or maxHeight prop');
        }
    
        const scrollOffset = ref(0);
        const start = ref(0);
        
        let wrap = props.scrollElement?.();
        let content = props.bodyElement?.();
        let measuredDataMap: IMeasuredDataMap = {};
        // 计算每项的高度和offset
        const measures = () => {
            measuredDataMap = {};
            const count = props.items.length;
            for (let i = 0; i < count; i++) {
                const prevItem = measuredDataMap[i - 1];
                const offset = prevItem ? prevItem.offset + prevItem.size : 0;
                measuredDataMap[i] = { size: props.itemEstimatedSize, offset };
            }
        }
    
        measures();
    
        // 二分法搜索
        const binarySearch = ({ low, high, scrollOffset }: any) => {
            let middle = 0;
            let currentOffset = 0;
            while (low <= high) {
                middle = low + Math.floor((high - low) / 2);
                currentOffset = measuredDataMap[middle]?.offset;
                if (currentOffset === scrollOffset) {
                    return middle;
                } else if (currentOffset < scrollOffset) {
                    low = middle + 1;
                } else {
                    high = middle - 1
                }
            }
            if (low > 0) {
                return low - 1;
            }
            return 0;
        }
    
        // 指数搜索
        const exponentialSearch = (scrollOffset: number) => {
            const itemCount = props.items.length;
            let interval = 1;
            let index = 0;
            while (index < itemCount && measuredDataMap[index]?.offset < scrollOffset) {
                index += interval;
                interval *= 2;
            }
            return binarySearch({
                low: Math.floor(index / 2),
                high: Math.min(index, itemCount - 1),
                scrollOffset
            })
        }
    
        // 获取第一个索引
        const getStartIndex = (scrollOffset: number) => {
            return exponentialSearch(scrollOffset);
        }
    
        // 获取容器最后一个索引
        const getEndIndex = (startIndex: number) => {
            const itemCount = props.items.length;
            if (itemCount === 0) {
                return 0;
            }
            // 获取可视区内开始的项
            const startItem = measuredDataMap[startIndex];
            // 可视区内最大的offset值
            const maxOffset = startItem?.offset + (wrap && wrap.clientHeight ? wrap.clientHeight : 0);
            // 开始项的下一项的offset，之后不断累加此offset，直到等于或超过最大offset，就是找到结束索引了
            let offset = startItem?.offset + startItem?.size;
            // 结束索引
            let endIndex = startIndex;
            // 累加offset
            while (offset <= maxOffset && endIndex < (itemCount - 1)) {
                endIndex++;
                const currentItem = measuredDataMap[endIndex];
                if (currentItem) {
                    offset += currentItem.size;
                }
            }
            return endIndex;
        };
    
        // 计算渲染的范围
        const getRangeToRender = (scrollOffset: number) => {
            const itemCount = props.items.length;
            if (itemCount === 0) {
                return [-1, -1];
            }
            const startIndex = getStartIndex(scrollOffset);
            const endIndex = getEndIndex(startIndex);
            return [
                Math.max(0, startIndex - (props.overscan || 3)),
                Math.min(itemCount - 1, endIndex + (props.overscan || 3)),
                startIndex,
                endIndex,
            ];
        };
    
        const estimatedHeight = () => {
            const itemCount = props.items.length;
            let h = 0;
            for (let i = 0; i < itemCount; i++) {
                h += measuredDataMap[i]?.size;
            }
            return h;
        }
    
        const height = ref(estimatedHeight());
    
        const scrollHandle = (event: any) => {
            const { scrollTop } = event.target;
            scrollOffset.value = scrollTop;
        }
    
        // 子元素项尺寸计算并重新计算offset
        const measureElement = (el: any, index: number) => {
            const rect = el.getBoundingClientRect();
            const item = measuredDataMap[index];
            if (item && item.size === rect.height) {
                return;
            }
            if (item) {
                item.size = rect.height;
            }
            const itemCount = props.items.length;
            for (let i = index + 1; i < itemCount; i++) {
                const item = measuredDataMap[i];
                const prevItem = measuredDataMap[i - 1];
                item.offset = prevItem ? prevItem.offset + prevItem.size : 0;
            }
    
            height.value = estimatedHeight();
        }
    
        // 子元素
        const getCurrentChildren = () => {
            const [startIndex, endIndex] = getRangeToRender(scrollOffset.value);
            start.value = startIndex;
    
            const items: any = [];
            if (startIndex >= 0) {
                for (let i = startIndex; i <= endIndex; i++) {
                    const itemStyle = {
                        width: '100%',
                    };
                    const children = slots.default?.();
                    
                    if (children && children.length) {
                        children.forEach((child: any) => {
                            if (typeof child.type === 'symbol' && child.children) {
                                child.children.forEach((subChild: any) => {
                                    subChild.props = {...subChild.props, 
                                        items: props.items,
                                        item: props.items[i],
                                        index: i,
                                        meta: measuredDataMap[i],
                                        style: itemStyle,
                                        ref: (el: any) => {
                                            Promise.resolve().then(() => {
                                                measureElement(el, i);
                                            })
                                        }
                                    }
                                    items.push(subChild);
                                });
                            } else {
                                child.props = {...child.props, 
                                    items: props.items,
                                    item: props.items[i],
                                    index: i,
                                    meta: measuredDataMap[i],
                                    style: itemStyle,
                                    ref: (el: any) => {
                                        Promise.resolve().then(() => {
                                            const ele = el.$el || el;
                                            measureElement(ele, i);
                                        })
                                    }
                                }
                                items.push(child);
                            }
                        })
                    }
                }
            }
            return items;
        }
    
        // 容器尺寸变化时重新计算当前可视区域元素的尺寸，触发高度变化，之后重新触发scroll变化重新计算可视区域索引,刷新可视区域元素
        const onWrapEntry = async (entry: ResizeObserverEntry) => {
            const [startIndex, endIndex] = getRangeToRender(scrollOffset.value);
            const childs = content.children;
            for (let i = startIndex; i <= endIndex; i++) {
                const el = childs[i - startIndex];
                if (el) {
                    measureElement(el, i);
                }
            }
            Promise.resolve().then(() => {
                scrollOffset.value = scrollOffset.value + 1;
            })
        }
        const ro = new ResizeObserver((entries) => {
            entries.forEach((entry) => onWrapEntry(entry));
        });

        const init = () => {
            wrap = props.scrollElement?.();
            content = props.bodyElement?.();

            // 容器尺寸变化
            ro.observe(wrap);
    
            // 列表元素大小变化时，导致容器高度不够或超长问题
            ro.observe(content);
    
            wrap.addEventListener('scroll', scrollHandle, false);

            watchEffect(() => {
                let originHeight: number | undefined = props.height;
                if (props.maxHeight) {
                    originHeight = height.value > props.maxHeight ? props.maxHeight : height.value;
                }
                wrap.style.height = originHeight + 'px';
            })

            watchEffect(() => {
                props.contentElement().style.height = height.value + 'px';
            })
        
            watchEffect(() => {
                props.bodyElement().style.transform = `translateY(${measuredDataMap[start.value]?.offset}px)`;
            })

            // 数据源更改触发
            watchEffect(() => {
                props.items;
                measures();
                wrap.scrollTop = 0;
                height.value = estimatedHeight();
            });
        }

        expose({
            init
        })
    
        onUnmounted(() => {
            ro.unobserve(wrap);
            ro.unobserve(content);
            wrap.removeEventListener('scroll', scrollHandle, false);
        })
    
        return () => <>
            {getCurrentChildren()}
        </>
    }
})